package YottaTron.communication;

import YottaTron.Sprites.PlayerSprite;
import YottaTron.*;

import YottaTron.Map.Map;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;

public class CommunicationUDP extends Thread {

    //KLIENT -> SERVER
    static final byte SIGN = 0;//SIGN; posila klient serveru pri prvnim pripojeni, server ODPOVI zpravou ID
    static final byte NICK = 1;//NICK,int id,String nick; server NEODPOVIDA
    static final byte UNSIGN = 2;//UNSIGN,ind id;

    //KLIENT <-> SERVER
    static final byte MSG = 3;//MSG,int id, String msg; klient serveru - vlastni id; NEODPOVIDA SE
    //MSG,int id, String msg; server klientu - id puvodce zpravy
    static final byte CLIENTS = 5;//CLIENTS; server posle vsem WHO se String seznamem nicku uzivatelu
    static final byte MULTICAST = 6;

    //SERVER -> KLIENT
    static final byte ID = 4;//ID,int id; posila server klientovi; klient zpet NEODPOVIDA
    //MULTICAST - prichazi vsem
    //static final byte CLIENTS = 5;
    static final byte NEW_PLAYER = 7;
    static final byte KEY_UP = 8;
    static final byte KEY_DOWN = 9;
    static final byte KEY_LEFT = 10;
    static final byte KEY_RIGHT = 11;
    static final byte MOUSE_LEFT_PRESSED = 12;
    static final byte MOUSE_AIM = 13;
    static final byte MAP = 14;
    static final byte MAP_OVER = 15;
    static final byte HUGE = 16;
    static final byte HUGE_NEXT = 17;
    static final byte HUGE_START = 18;
    static final byte SYNCHRONIZE = 19;
    static final byte CLIENT_INPUT = 20;
    static final byte BIT = 21;
    static final byte DEAD = 22;
    static final byte SPEED = 23;
    static final byte FINISHED_ACTION = 24;
    static final byte TIME_STAMP = 25;
    static final byte POSITION = 26;
    static final int PACKET_SIZE = 512;
    ChatGui gui;
    public DatagramChannel udpChannel;
    private ByteBuffer inBuffer;
    /* pokud se pri naplnovani outputBufferu zavola asynchronne posilani jineho packetu,
     * nepremaze se jediny outputBuffer, ale zacne se naplnovat jiny z pole outBuffers
     * s vyuzitim promenne actualOutBuffer... tu bude mozna potreba zamykat "mutexem"
     * vyhoda ... alokace nekolika bufferu jen na zacatku
     */
    private ByteBuffer[] outBuffers;
    // zadny globalni buffery!!! pri asynchronnim posilani vznikaji IllegalArgument a BufferUnderflow ex..
    public SocketAddress serverAddress;
    private String host;
    public int serverPort;
    public int clientID; // id pridelene serverem
    public int mapSizeBytes;
    public ArrayList<byte[]> cache = new ArrayList<byte[]>();
    Runner runner;
    ByteArrayOutputStream bytestream = new ByteArrayOutputStream(PACKET_SIZE);
    DataOutputStream datastream = new DataOutputStream(bytestream);
    public int hugeDataReceived;
    public int hugeDataExpected;
    byte[] hugeData;
    public int packetsSent = 0;
    public float lag = 0.0f;

    /* watchdog hlida jestli neuplynul prilis velky cas od odeslani posledniho HugeDataRequestu
     * pokud ano, posle novy Request serveru.
     */
    WatchDog watchDog;
    private int numberOfBuffers = 5;
    private int actualOutBuffer = 0;
    public float timeDiff;
    public float timeDelay;

    public CommunicationUDP(String host, int serverPort, ChatGui gui, Runner runner) {
        this.gui = gui;
        this.runner = runner;

        initUDPChannel();
        // kvuli reseni asynchronniho posilani packetu
        // a aby se pri kazdem send nemusel alokovat novy buffer
        outBuffers = new ByteBuffer[numberOfBuffers];
        for (int i = 0; i < outBuffers.length; i++) {
            outBuffers[i] = ByteBuffer.allocateDirect(PACKET_SIZE); //alokovat buffer
        }
        inBuffer = ByteBuffer.allocateDirect(CommunicationUDP.PACKET_SIZE);
        this.start();

        connectToServer(host, serverPort);
    }

    public void initUDPChannel() {
        try {
            udpChannel = DatagramChannel.open();
            udpChannel.configureBlocking(true);
            // port 0 selects any available port
            udpChannel.socket().bind(new InetSocketAddress(0));
        } catch (IOException ex) {
            System.out.println("klient: chyba vytvareni datagram channelu");
        }
    }

    public void connectToServer(String host, int serverPort) {
        this.host = host;
        this.serverPort = serverPort;
        serverAddress = new InetSocketAddress(host, serverPort);

        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(SIGN);
        outBuffers[a].flip();
        System.out.println("client connecting to server");

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendUdpPacket(SocketAddress clientAddress, ByteBuffer buffer) {
        try {
            //System.out.println("buffer.toString() " + buffer.toString());
//            System.out.println("actualOutBuffer " + actualOutBuffer);
            udpChannel.send(buffer, clientAddress);
            packetsSent++;
        } catch (IOException e) {
            System.out.println("chyba pri posilani udp packetu");
            e.printStackTrace(System.err);
        }

    }

    //nefunguje. ByteDataChannel vzdycky posle min byte.. ale lze zabalit bity
    // do bytu pomoci << >> & |
    public void sendBit(byte b) {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(BIT);
        b = (byte) (b << 7);
        outBuffers[a].put((byte) 1);
        System.out.println("sendBit before flip - outBuffer.toString() " + outBuffers[a].toString());
        outBuffers[a].flip();
        System.out.println("sendBit outBuffer.toString() " + outBuffers[a].toString());
        System.out.println("sendBit outBuffer.get() " + outBuffers[a].get());
        System.out.println("sendBit outBuffer.get() " + outBuffers[a].get());

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendClientsListRequest() {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(CLIENTS);
        outBuffers[a].flip();

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void unsignFromServer() {
        System.out.println("unsigning");
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(UNSIGN);
        System.out.println("unsign id " + clientID);
        outBuffers[a].putInt(clientID);
        outBuffers[a].flip();


        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
        System.out.println("msg UNSING sent");
    }

    public void sendNick(String nick) {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(NICK);
        outBuffers[a].putInt(clientID);
        outBuffers[a].put(nick.getBytes());
        outBuffers[a].flip();
        System.out.println("client ID ze sendNick = " + clientID);
        System.out.println("sending nick " + nick);

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendTimeStamp() {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(TIME_STAMP);
        outBuffers[a].putInt(clientID);
        outBuffers[a].putFloat(runner.globalTimer.getTime());
        outBuffers[a].flip();
        System.out.println("sending timestamp");
        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendMessage(String msg) {
        if (msg.equals("")) {
            JOptionPane.showMessageDialog(null,
                    "No message entered", "Send Message Error",
                    JOptionPane.ERROR_MESSAGE);
        } else {
            int a = actualOutBuffer;
            actualOutBuffer++;
            outBuffers[a].clear();
            outBuffers[a].put(MSG);
            outBuffers[a].putInt(clientID);
            outBuffers[a].put(msg.getBytes());
            outBuffers[a].flip();
            System.out.println("clientID " + clientID + " sending message: " + msg);

            sendUdpPacket(serverAddress, outBuffers[a]);
            actualOutBuffer--;
        }
    }  // end of sendMessage()

    // byte[] weapons ... pole o 10 polozkach = maximalne 10 zbrani
    public void sendNewPlayer(double x, double y, double direction, byte[] weapons) {
        System.out.println("sending NEW_PLAYER");
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(NEW_PLAYER);
        outBuffers[a].putInt(clientID);
        outBuffers[a].putDouble(x);
        outBuffers[a].putDouble(y);
        outBuffers[a].putDouble(direction);
        outBuffers[a].put(weapons);
        outBuffers[a].flip();

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendDead() {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(DEAD);
        outBuffers[a].putInt(clientID);
        outBuffers[a].flip();

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendMouseAim(short aim) {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(MOUSE_AIM);
        outBuffers[a].putInt(clientID);
        outBuffers[a].putShort(aim);
        outBuffers[a].flip();

        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendHugeDataRequest() {
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(HUGE_NEXT);
        outBuffers[a].putInt(clientID);
        outBuffers[a].putInt(hugeDataReceived);
        outBuffers[a].flip();
        watchDog.resetTime();
        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void sendClientPacket(float timeStamp) {
        PlayerSprite player = runner.getControllablePlayer();
        int a = actualOutBuffer;
        actualOutBuffer++;
        outBuffers[a].clear();
        outBuffers[a].put(CLIENT_INPUT);
        outBuffers[a].putInt(clientID);
        outBuffers[a].putDouble(player.direction);
        outBuffers[a].putDouble(player.speed);
        outBuffers[a].putShort(player.netAimAngle);
        outBuffers[a].put(player.isMouseLeftPressed() ? (byte)1 : (byte)0);
        outBuffers[a].putFloat(timeStamp);
        outBuffers[a].flip();
        sendUdpPacket(serverAddress, outBuffers[a]);
        actualOutBuffer--;
    }

    public void processHugeData() {
        byte[] temp = new byte[0];
        for (Iterator<byte[]> it = cache.iterator(); it.hasNext();) {
            byte[] bs = it.next();
            temp = joinArrays(temp, bs);
        }
        System.out.println("Prijata cela mapa - probiha zpracovani " + temp.length + " bytu");
        ByteArrayInputStream bis = new ByteArrayInputStream(temp);
        try {
            ObjectInputStream ois = new ObjectInputStream(bis);
            runner.map = (Map) ois.readObject();

            ois.close();
            runner.mapReady = true;
            System.out.println("a jedeem");
        } catch (Exception e) {
            e.printStackTrace();
        }
        for (int i = 0; i < temp.length; i++) {
        }
    }

    public byte[] joinArrays(byte[] a1, byte[] a2) {
        byte[] result = new byte[a1.length + a2.length];
        for (int i = 0; i < a1.length; i++) {
            result[i] = a1[i];
        }
        for (int i = 0; i < a2.length; i++) {
            result[i + a1.length] = a2[i];
        }
        return result;
    }

    @Override
    public void run() {
        byte[] buf;
        String msg;
        byte bool;
        PlayerSprite player;
        SocketAddress clientAddress = null;
//
//        try {

        while (true) {
            try {
                inBuffer.clear();
//                System.out.println("waiting for packet from server");
                clientAddress = udpChannel.receive(inBuffer); //přijmout UDP paket
                inBuffer.flip();
                if (clientAddress.equals(serverAddress)) {
                    switch (inBuffer.get()) {
                        case ID:
                            clientID = inBuffer.getInt();
                            System.out.println("ID from server - id: " + clientID);

                            sendTimeStamp();

                            break;
                        case MSG:
                            System.out.println("MSG from server");
                            int id = inBuffer.getInt();
                            //prevod zbytku ByteBufferu na String
                            buf = new byte[inBuffer.remaining()];
                            inBuffer.get(buf);
                            msg = new String(buf);
                            System.out.println("id " + id + " msg " + msg);
                            gui.taChat.append(msg + "\n");
                            gui.taChat.setCaretPosition(gui.taChat.getText().length());
                            break;
                        case NEW_PLAYER:
                            System.out.println("NEW_PLAYER from server");
                            id = inBuffer.getInt();
                            double x = inBuffer.getDouble();
                            double y = inBuffer.getDouble();
                            double direction = inBuffer.getDouble();

                            // precteme pole zbrani
                            byte[] weapons = new byte[10];
                            for (int i = 0; i < weapons.length; i++) {
                                weapons[i] = inBuffer.get();
                            }
                            System.out.println("new player: x" + x + " y " + y + " direction " + direction + " clientID " + id);
                            if (id != clientID) {
                                runner.addPlayer(x, y, direction, id, weapons); //hrac se prida do seznamu hracu na strane klienta
                            }
                            break;
                        case UNSIGN:
                            System.out.println("UNSIGN");
                            id = inBuffer.getInt();
                            runner.removePlayer(id);
                            System.out.println("client with id " + id + " removed");
                            break;

                        case TIME_STAMP:
                            sendNick(gui.tfNick.getText());
                            new Thread(runner).start();
                            break;

                        case HUGE_START:
                            hugeDataExpected = inBuffer.getInt();
                            watchDog = new WatchDog(this, 500);
                            sendHugeDataRequest();
                            System.out.println("ocekavam prisun " + hugeDataExpected + " bytu");
                            break;

                        case HUGE:
                            int hugeDataNum = inBuffer.getInt();//poradi bytu, kterym zacina tento packet
                            System.out.println("Prijimam paket zacinajici bytem " + hugeDataNum);
                            if (hugeDataNum == hugeDataReceived) {
                                System.out.println("paket je spravny");
                                byte[] input = new byte[inBuffer.remaining()];
                                inBuffer.get(input);
                                hugeDataReceived += input.length;
//                                System.out.println(input[0]);
                                cache.add(input);
                                if (hugeDataReceived == hugeDataExpected) {
                                    System.out.println("vsechny pakety prijaty");
                                    watchDog.end();
                                    watchDog = null;
                                    processHugeData();
                                } else {
                                    sendHugeDataRequest();
                                }
                            } else {
                                //pokud by byla prijata vsechna pozadovana data, neposilame dalsi request
                                if (hugeDataReceived != hugeDataExpected) {
                                    System.out.println("spatny paket - zadam o spravny");
                                    sendHugeDataRequest();
                                }
                            }
                            runner.repaint();//vykresli se postup prijimanych dat
                            break;


                        

                        case SYNCHRONIZE:
                            while(inBuffer.hasRemaining()) {
                                id= inBuffer.getInt();
                                
                                float timeStamp = inBuffer.getFloat();
                                x = inBuffer.getDouble();
                                y = inBuffer.getDouble();
                                direction = inBuffer.getDouble();
                                double speed = inBuffer.getDouble();
                                short aimAngle = inBuffer.getShort();
                                byte mouseLeftPressed = inBuffer.get();
                                int hitPoints = inBuffer.getInt();
                                PlayerSprite s = runner.getSpriteByID(id);
                                if (s != null) {
                                    s.serverX = x;
                                    s.serverY = y;
                                    s.serverDirection = direction;
                                    s.serverSpeed = speed;
                                    s.netAimAngle = aimAngle;
                                    s.setHitPoints(hitPoints);
                                    s.shooting = mouseLeftPressed==1 ? true : false;
                                    
//                                s.doActionsFromTime(timeStamp);
                                    //System.out.println("received synchronize info for player " + id + " x/y = " + x + "/" + y + " direction = " + direction);
                                    runner.handleSynchronize = true;
                                    s.synchroTime = timeStamp;

                                }
                            }
                            break;


                        default:
                            System.out.println("neznama zprava od serveru zacinajici " + inBuffer.get(0));
                            break;
                    }
                }
            } catch (IOException ex) {
                Logger.getLogger(CommunicationUDP.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

//        } catch (Exception e) {
//            System.out.println("chyba v run: " + e);
//        }
    } // end of run()
}
